/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2012-2018. All rights reserved.
 * Description: tune for the emmc/sd card
 * Author: wenhao w00470829
 * Create: 2019-03-22
 * Notes: None
 * History: 2019-03-22 wenhao w00470829 first release
 */
#include "asm/delay.h"
#include "mmc_core.h"
#include "sdhci.h"
#include "sdhci_reg.h"
#include "proc/sdhci_proc.h"

#include "linux/device.h"
#include "linux/platform_device.h"
#include "linux/module.h"

#define PHASE_SCALE (32)
#define NOT_FOUND (-1)
#define MAX_TUNING_NUM  (1)

#define SDIO_SAMPLB_DLL_CLK (8)
#define PHASE_SCALE_GAP (16)

#define PHASE_SCALE_TIMES (4)
#define DIV_MIDDLE (2)

static void SDHCI_EnableEdgeTune(struct sdhc_host *host)
{
    unsigned int reg;
    unsigned int offset[] = {PERI_SDIO0_SAMPLB_DLL_CTRL, PERI_SDIO1_SAMPLB_DLL_CTRL};

    if (host->id >= ARRAY_SIZE(offset)) {
        return;
    }
    reg = readl(offset[host->id]);
    reg = (reg & (~SDIO_SAMPLB_DLL_CLK_MASK)) | SDIO_SAMPLB_SEL(SDIO_SAMPLB_DLL_CLK);
    writel(reg, offset[host->id]);
    reg = sdhc_readl(host, SDHC_MULTI_CYCLE);
    reg |= SDHC_EDGE_DETECT_EN | SDHC_DATA_DLY_EN;
    reg &= ~SDHC_CMD_DLY_EN;
    sdhc_writel(host, reg, SDHC_MULTI_CYCLE);
}

static void SDHCI_DisEdgeTune(struct sdhc_host *host)
{
    unsigned int reg;

    reg = sdhc_readl(host, SDHC_MULTI_CYCLE);
    reg &= ~SDHC_EDGE_DETECT_EN;
    sdhc_writel(host, reg, SDHC_MULTI_CYCLE);
}

static void SDHCI_PreTune(struct sdhc_host *host)
{
    unsigned int val;

    val = sdhc_readl(host, SDHC_INT_ENABLE);
    sdhc_writel(host, val | SDHC_INT_DATA_AVAIL, SDHC_INT_ENABLE);
    val = sdhc_readl(host, SDHC_SIGNAL_ENABLE);
    sdhc_writel(host, val | SDHC_INT_DATA_AVAIL, SDHC_SIGNAL_ENABLE);

    SDHCI_WaitDrvDllLock(host);
    SDHCI_EnSamplDllSlave(host);
    SDHCI_EnableSample(host);
}

static void SDHCI_PostTune(struct sdhc_host *host)
{
    unsigned int val;
    unsigned short ctrl;

    ctrl = sdhc_readw(host, SDHC_HOST_CONTROL2);
    ctrl |= SDHC_CTRL_TUNED_CLK;
    sdhc_writew(host, ctrl, SDHC_HOST_CONTROL2);

    val = sdhc_readl(host, SDHC_INT_ENABLE);
    val &= ~SDHC_INT_DATA_AVAIL;
    sdhc_writel(host, val, SDHC_INT_ENABLE);
    val = sdhc_readl(host, SDHC_SIGNAL_ENABLE);
    val &= ~SDHC_INT_DATA_AVAIL;
    sdhc_writel(host, val, SDHC_SIGNAL_ENABLE);
}

static void SDHCI_SelectSamplPhase(struct sdhc_host *host,
                                   unsigned int phase)
{
    SDHCI_CardClk(host, 0);
    sdhci_hisi_set_samp_phase(host, phase);
    SDHCI_WaitSamplDllSlaveReady(host);
    SDHCI_CardClk(host, 1);
    udelay(1);
}

static int SDHCI_SendTune(struct sdhc_host *host, unsigned int opcode)
{
    int count, err;

    count = 0;
    do {
        err = mmc_send_tuning(host->mmc, opcode, NULL);
        if (err) {
            break;
        }
        count++;
    } while (count < MAX_TUNING_NUM);

    return err;
}

void SDHCI_DoTune(struct sdhc_host *host, unsigned int opcode, unsigned int start, unsigned int end)
{
    int err;
    int prevError = 0;
    unsigned int phase;
    unsigned int fall, rise, fallUpdateFlag, index;

    fall = start;
    rise = end;
    fallUpdateFlag = 0;
    for (index = start; index <= end; index++) {
        SDHCI_SelectSamplPhase(host, index % PHASE_SCALE);

        err = SDHCI_SendTune(host, opcode);
        if (err) {
            mmc_trace(MMC_TRACE_DEBUG,
                      "send tuning CMD%u fail! phase:%d err:%d\n",
                      opcode, index, err);
        }
        if (err && index == start) {
            if (!fallUpdateFlag) {
                fallUpdateFlag = 1;
                fall = start;
            }
        } else {
            if (!prevError && err && !fallUpdateFlag) {
                fallUpdateFlag = 1;
                fall = index;
            }
        }

        if (prevError && !err) {
            rise = index;
        }

        if (err && index == end) {
            rise = end;
        }
        prevError = err;
    }

    phase = ((fall + rise) / DIV_MIDDLE + PHASE_SCALE_GAP) % PHASE_SCALE;
    mmc_trace(MMC_TRACE_DEBUG,
              "host%d: tuning done! valid phase shift [%d, %d] Final Phase:%d\n",
              host->id, rise % PHASE_SCALE, fall % PHASE_SCALE, phase);
    host->tuningPhase = phase;
    SDHCI_SelectSamplPhase(host, phase);
    SDHCI_PostTune(host);
}

int MMC_ExeTuning(struct mmc_host *mmc, unsigned int  opcode)
{
    struct sdhc_host *host = mmc->priv;
    unsigned int index, val;
    unsigned int found = 0;
    unsigned int prevFound = 0;
    unsigned int edgeP2F = 0;
    unsigned int start = 0;
    unsigned int edgeF2P = PHASE_SCALE / PHASE_SCALE_TIMES;
    unsigned int end = PHASE_SCALE / PHASE_SCALE_TIMES;
    int err;

    SDHCI_PreTune(host);
    SDHCI_EnableEdgeTune(host);
    for (index = 0; index <= end; index++) {
        SDHCI_SelectSamplPhase(host, index * PHASE_SCALE_TIMES);
        err = mmc_send_tuning(mmc, opcode, NULL);
        if (!err) {
            val = sdhc_readl(host, SDHC_MULTI_CYCLE);
            found = val & SDHC_FOUND_EDGE;
        } else {
            found = 1;
        }

        if (prevFound && !found) {
            edgeF2P = index;
        }
        else if (!prevFound && found) {
            edgeP2F = index;
        }
        if ((edgeP2F != start) && (edgeF2P != end)) {
            break;
        }
        prevFound = found;
        found = 0;
    }

    if ((edgeP2F == start) && (edgeF2P == end)) {
        mmc_err("host%d: tuning failed! can not found edge!\n",
                host->id);
        return -1;
    }
    SDHCI_DisEdgeTune(host);

    start = edgeP2F * PHASE_SCALE_TIMES;
    end = edgeF2P * PHASE_SCALE_TIMES;
    if (end <= start) {
        end += PHASE_SCALE;
    }

    SDHCI_DoTune(host, opcode, start, end);
    return 0;
}
